Skip to content

fix(claude): emit plan events for TodoWrite during input streaming#1541

Merged
juliusmarminge merged 19 commits intopingdotgg:mainfrom
TimCrooker:fix/claude-todowrite-plan-events
Apr 14, 2026
Merged

fix(claude): emit plan events for TodoWrite during input streaming#1541
juliusmarminge merged 19 commits intopingdotgg:mainfrom
TimCrooker:fix/claude-todowrite-plan-events

Conversation

@TimCrooker
Copy link
Copy Markdown
Contributor

@TimCrooker TimCrooker commented Mar 29, 2026

What Changed

Claude's TodoWrite tool calls now emit turn.plan.updated events during input streaming so the plan sidebar shows task progress in real-time. The plan event fires alongside the existing tool lifecycle events, not instead of them.

Related fixes:

  • item.completed activities now forward the data field to the UI, matching item.updated
  • task.completed entries show up in the work log (previously filtered out with task.started)
  • Task activity labels use payload.summary when available instead of the generic activity summary
  • Agent/subagent tool summaries show the human-readable description instead of raw JSON
  • Plan state persists across follow-up turns so tasks don't vanish when you send a message
  • Sidebar auto-opens when task steps arrive, respects user dismiss
  • Sidebar shows "Tasks" outside plan mode, "Plan" inside it -- context-aware, no new UI components

Closes #1539

Why

TodoWrite gets classified as file_change in classifyToolItemType because the name contains "write". It renders as a generic "File change - TodoWrite: {raw JSON}" line in the work log. No turn.plan.updated event gets emitted, so the plan sidebar never activates for Claude sessions. This is core functionality that works for Codex but is completely broken for Claude.

There's an existing PR for this (#1387) that intercepts at tool result time and replaces the normal item.updated/content.delta emissions. This PR takes a different approach:

  1. Doesn't suppress existing events. The plan event fires alongside normal tool lifecycle, so downstream consumers aren't affected.
  2. Streams in real-time. Emitting during input_json_delta means the sidebar populates as Claude writes the input, not after the full round-trip.
  3. Persists across turns. deriveActivePlanState falls back to the most recent plan from any previous turn. Without this, the sidebar clears every time you send a follow-up.
  4. Context-aware labeling. Instead of forcing users into plan mode to see tasks, the sidebar dynamically labels itself "Tasks" vs "Plan" based on whether you're in plan mode. Same component, same UX, just the right label for the context.

Validated with bun fmt, bun lint, bun typecheck, and bun run test (all passing).

UI Changes

Before -- TodoWrite is invisible to the sidebar

Tasks exist but are completely invisible. There is no way to see them.

Before: no sidebar, TodoWrite tasks hidden

After -- Tasks stream in live without plan mode

Tasks are now streamed in real-time and persist between turns.

After: sidebar open showing task steps with status

After -- Button label adapts to context

When the session has tasks but no plan, the button shows "Tasks":

Composer toolbar showing Tasks button

When the session is in plan mode, the same button shows "Plan":

Screenshot 2026-03-29 at 9 11 52 PM

Checklist

  • This PR is small and focused
  • I explained what changed and why
  • I included before/after screenshots for any UI changes
  • I included a video for animation/interaction changes

Note

Medium Risk
Touches provider event emission/ingestion and client-side plan/worklog derivation, so regressions could affect real-time activity rendering and sidebar behavior across turns.

Overview
Claude input_json_delta handling now detects TodoWrite tool calls, extracts todo steps, and emits turn.plan.updated events during streaming (including a default "Task" label for blank items), while tool summaries for collab-agent invocations prefer human-readable description/prompt over raw JSON.

Runtime ingestion now forwards payload.data on item.completed activities, and the web app updates plan/task presentation: active plan state falls back to the latest plan from prior turns, the plan sidebar auto-opens when new steps arrive (respecting user dismissal), and the sidebar/button label switches between "Plan" and "Tasks" based on context.

Work log derivation is adjusted to include task.completed (still omitting task.started), prefer payload.summary/payload.detail as task labels, and render task.progress with a "thinking" tone.

Reviewed by Cursor Bugbot for commit af3e93a. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Emit turn.plan.updated events for TodoWrite tool input during Claude streaming

  • During input_json_delta streaming in ClaudeAdapter.ts, detects TodoWrite tools and emits turn.plan.updated runtime events with normalized plan steps (blank content becomes "Task", status values mapped to completed/inProgress/pending).
  • Updates session-logic.ts so deriveActivePlanState falls back to the most recent plan from a prior turn when the current turn has none, and deriveWorkLogEntries now includes task.completed entries with labels sourced from payload.summary or payload.detail.
  • The plan sidebar in ChatView.tsx auto-opens when plan/task steps arrive for the current turn (unless dismissed), and its label toggles between "Plan" and "Tasks" based on context.
  • Behavioral Change: the plan sidebar will open automatically on new plan steps, which may be unexpected for users who have not explicitly dismissed it.

Macroscope summarized af3e93a.

When Claude calls TodoWrite, emit turn.plan.updated events during input
streaming so the plan sidebar displays Claude's todos the same way it
already works for Codex plan steps. Events are emitted alongside existing
tool lifecycle events, not as a replacement.

Also passes through the data field on item.completed activities to match
item.updated behavior, and auto-opens the plan sidebar when plan steps
arrive.

Closes pingdotgg#1539
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 29, 2026

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 76bcff43-f45a-4fa6-94f6-75948292b898

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions bot added size:M 30-99 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list. labels Mar 29, 2026
Plan state now falls back to the most recent plan from any previous
turn when the current turn has no plan activity, so TodoWrite tasks
stay visible across follow-up messages. Simplified redundant isTodoTool
check.
@TimCrooker TimCrooker marked this pull request as ready for review March 29, 2026 18:21
Copilot AI review requested due to automatic review settings March 29, 2026 18:21
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR wires Claude’s TodoWrite tool into the existing plan sidebar by emitting turn.plan.updated events during Claude input streaming, and includes several related UX/data-flow fixes so todos and task activity render consistently in the UI.

Changes:

  • Emit turn.plan.updated during Claude input_json_delta processing for TodoWrite, without suppressing existing tool lifecycle events.
  • Persist/restore plan state across turns and auto-open the plan sidebar when plan steps arrive.
  • Improve work log rendering (include task.completed, prefer task payload summaries) and forward data for item.completed activities.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
apps/web/src/session-logic.ts Persist active plan across turns; include task.completed; adjust task entry label/tone logic.
apps/web/src/session-logic.test.ts Add coverage for plan fallback and updated work log filtering/label behavior.
apps/web/src/components/ChatView.tsx Auto-open plan sidebar when an active plan appears (respecting dismiss state).
apps/server/src/provider/Layers/ClaudeAdapter.ts Emit turn.plan.updated events for TodoWrite during streamed tool input parsing; improve tool request summaries for agent tools.
apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts Forward payload.data for item.completed tool lifecycle activities.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Only force "thinking" tone for task.progress, not task.completed, so
failed tasks preserve their error tone. Also check payload.detail for
task labels since task.completed stores its summary there. Add
regression test for failed task.completed rendering.
@github-actions github-actions bot added size:L 100-499 changed lines (additions + deletions). and removed size:M 30-99 changed lines (additions + deletions). labels Mar 29, 2026
Use a sentinel string when turnId is null so the dismissed ref still
gets set, preventing the auto-open effect from immediately reopening
the sidebar.
Apply the same __dismissed__ sentinel to the onClose handler on the
plan sidebar X button, matching the fix already applied to
togglePlanSidebar.
Use the same turnKey fallback chain (activePlan.turnId ??
sidebarProposedPlan?.turnId ?? "__dismissed__") in both the auto-open
effect and the dismiss handlers so they always match.
@TimCrooker TimCrooker closed this Mar 29, 2026
@TimCrooker TimCrooker reopened this Mar 30, 2026
Dynamically switch the sidebar label between "Plan" and "Tasks" based
on context. When a proposed plan exists or the user is in plan mode,
the label reads "Plan". Otherwise it reads "Tasks". Applies to the
composer button, compact menu, sidebar badge, and aria labels.
TimCrooker and others added 2 commits March 29, 2026 20:23
…tail

Only auto-open the sidebar for plans from the current turn, not
fallbacks from previous turns. Add explicit parentheses to the label
ternary for clarity. Skip detail assignment when the detail text is
already used as the label to avoid duplication in the work log.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@TimCrooker
Copy link
Copy Markdown
Contributor Author

TimCrooker commented Mar 30, 2026

Quick summary of the iteration here since there are a few rounds of commits:

Started by wiring TodoWrite into the existing plan event path so the sidebar actually shows Claude's tasks. Bugbot caught a few real issues (dismiss key mismatch, error tone override, label/detail duplication, auto-open on thread switch) each one got a focused fix.

Midway through I thought this would need a dedicated task UI component separate from the plan sidebar, since the plan panel was designed around Codex's plan mode. Almost closed it to go rethink.

Then realized the simpler answer: just dynamically label the sidebar "Tasks" vs "Plan" based on context. Same component, no new UI, no plan mode required. Users get task visibility without being forced into a workflow they didn't ask for.

Current state: all Bugbot feedback resolved, tests passing, parentheses fix for the label ternary pushed. Ready for review.

Copy link
Copy Markdown
Contributor

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

@juliusmarminge juliusmarminge self-requested a review April 6, 2026 17:30
@macroscopeapp
Copy link
Copy Markdown
Contributor

macroscopeapp bot commented Apr 14, 2026

Approvability

Verdict: Needs human review

Despite the 'fix' prefix, this PR introduces new feature capability: emitting plan events during TodoWrite streaming, auto-opening the plan sidebar, and changing how plans persist across turns. These are significant runtime behavior changes spanning server and client code that warrant human review.

You can customize Macroscope's approvability policy. Learn more.

Co-authored-by: codex <codex@users.noreply.github.com>
@juliusmarminge juliusmarminge merged commit 0d28026 into pingdotgg:main Apr 14, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L 100-499 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Claude TodoWrite updates don't flow to the plan sidebar

3 participants